import * as THREE from "../../../modules/three/three.module.js";
import SprayPath, {NozzleActionEnum} from "../model/SprayPath.js";
import {defaultAreaColor} from "../lib/util/StaticMethods.js";
import {removeArrayItem} from "../lib/util/ToolMethods.js";

const _raycaster = new THREE.Raycaster()
export const DirectEnum = {
  horizontal: 'horizontal',
  vertical: 'vertical',
}
export class SprayConfig {
  // 一次喷漆在 y轴的覆盖距离，以米为单位
  rectWidth = 0.35
  // 一次喷漆在 x轴的覆盖距离，以米为单位
  rectHeight = 0.06
  // 离墙距离
  sprayDistance = 2.0
  // 换行距离，当小于等于零时，取rectWidth的一半
  spacingWidth = 0
  direction = DirectEnum.vertical
  // 起飞点，世界零变换坐标系
  flyHomePoint = new THREE.Vector3()
  // 航线的外扩距离
  expand = 0
  static Default = new SprayConfig()
}
/**
 * 根据工作区生成喷涂航线，只针对不带孔的矩形工作区
 * @param {SprayConfig} config
 * @param {WorkArea} sprayArea
 * @return {THREE.Vector3[]} 世界零变换坐标系下的航点列表
 */
function computeRectSprayPath( config, sprayArea) {
  const {rectWidth, direction, expand} = config
  const spacingWidth = config.spacingWidth <= 0 ? rectWidth * 0.5 : config.spacingWidth
  sprayArea.updateLocalBox()
  sprayArea.initWidthAndHeight()
  const width = sprayArea._width
  const height = sprayArea._height
  let sprayTurns = 0 // 趟数
  let remain = 0
  const localHomePoint = new THREE.Vector3().copy(config.flyHomePoint).sub(sprayArea.getCenterInWorld())
  localHomePoint.transformDirection(sprayArea.planeMatInv)
  const needReverseY = localHomePoint.y > 0
  const needReverseX = localHomePoint.x > 0
  let bigToSmall = false
  const minX = sprayArea.localBox3.min.x
  const maxX = sprayArea.localBox3.max.x
  const minY = sprayArea.localBox3.min.y
  const maxY = sprayArea.localBox3.max.y
  const wallPath = []
  if (direction === DirectEnum.vertical) { // 纵向优先
    sprayTurns = Math.floor(width / spacingWidth)
    remain = width - sprayTurns * spacingWidth
    let v0, v1
    bigToSmall = needReverseY
    for (let i = 0; i < sprayTurns + 1; i++) {
      const dx = i * spacingWidth
      if (needReverseX) {
        v0 = new THREE.Vector3(maxX - dx, maxY, 0)
        v1 = new THREE.Vector3(maxX - dx, minY, 0)
      } else {
        v0 = new THREE.Vector3(minX + dx, maxY, 0)
        v1 = new THREE.Vector3(minX + dx, minY, 0)
      }
      if (expand > 0) {
        const v0_ = new THREE.Vector3(v0.x, v0.y + expand, 0)
        const v1_ = new THREE.Vector3(v1.x, v1.y - expand, 0)
        bigToSmall ? wallPath.push(v0_, v0, v1, v1_) : wallPath.push(v1_, v1, v0, v0_)
      } else {
        bigToSmall ? wallPath.push(v0, v1) : wallPath.push(v1, v0)
      }
      bigToSmall = !bigToSmall
    }
    if (remain > 0) {
      if (needReverseX) {
        v0 = new THREE.Vector3(minX, maxY, 0)
        v1 = new THREE.Vector3(minX, minY, 0)
      } else {
        v0 = new THREE.Vector3(maxX, maxY, 0)
        v1 = new THREE.Vector3(maxX, minY, 0)
      }
      if (expand > 0) {
        const v0_ = new THREE.Vector3(v0.x, v0.y + expand, 0)
        const v1_ = new THREE.Vector3(v1.x, v1.y - expand, 0)
        bigToSmall ? wallPath.push(v0_, v0, v1, v1_) : wallPath.push(v1_, v1, v0, v0_)
      } else {
        bigToSmall ? wallPath.push(v0, v1) : wallPath.push(v1, v0)
      }
    }
  } else { // 横向优先
    sprayTurns = Math.floor(height / spacingWidth)
    remain = height - sprayTurns * spacingWidth
    let v0, v1
    bigToSmall = needReverseX
    for (let i = 0; i < sprayTurns + 1; i++) {
      const dy = i * spacingWidth
      if (needReverseY) {
        v0 = new THREE.Vector3(maxX, maxY - dy, 0)
        v1 = new THREE.Vector3(minX, maxY - dy, 0)
      } else {
        v0 = new THREE.Vector3(maxX, minY + dy, 0)
        v1 = new THREE.Vector3(minX, minY + dy, 0)
      }
      if (expand > 0) {
        const v0_ = new THREE.Vector3(v0.x + expand, v0.y, 0)
        const v1_ = new THREE.Vector3(v1.x - expand, v1.y, 0)
        bigToSmall ? wallPath.push(v0_, v0, v1, v1_) : wallPath.push(v1_, v1, v0, v0_)
      } else {
        bigToSmall ? wallPath.push(v0, v1) : wallPath.push(v1, v0)
      }
      bigToSmall = !bigToSmall
    }
    if (remain > 0) {
      if (needReverseY) {
        v0 = new THREE.Vector3(maxX, minY, 0)
        v1 = new THREE.Vector3(minX, minY, 0)
      } else {
        v0 = new THREE.Vector3(maxX, maxY, 0)
        v1 = new THREE.Vector3(minX, maxY, 0)
      }
      if (expand > 0) {
        const v0_ = new THREE.Vector3(v0.x + expand, v0.y, 0)
        const v1_ = new THREE.Vector3(v1.x - expand, v1.y, 0)
        bigToSmall ? wallPath.push(v0_, v0, v1, v1_) : wallPath.push(v1_, v1, v0, v0_)
      } else {
        bigToSmall ? wallPath.push(v0, v1) : wallPath.push(v1, v0)
      }
    }
  }
  return wallPath
}

/**
 * @param {WorkArea} sprayArea 工作区
 * @param {SprayConfig} config 航线配置
 * @param {THREE.Vector3[]} wallPath 工作区本地空间的喷头坐标，是输出 又是输入
 * @param {THREE.Vector3[]} dronePath 输出
 * @param {THREE.Mesh|null} convexMesh 凸包网格
 */
function adjustWallPathBySurface(sprayArea, config, wallPath, dronePath, convexMesh) {
  if (!convexMesh || !sprayArea.fromSurface) {
    return false
  }
  _raycaster.ray.direction.copy(sprayArea.facing).negate()
  wallPath.forEach(pt => {
    const pt2 = pt.clone()
    pt2.z = -config.sprayDistance
    pt2.applyMatrix4(sprayArea.planeMat)
    dronePath.push(pt2)
    pt.applyMatrix4(sprayArea.planeMat)
    _raycaster.ray.origin.copy(pt)
    const intersects = _raycaster.intersectObject(convexMesh, false)
    if (intersects.length) {
      const offset = intersects[0].point.clone().sub(pt)
      pt.add(offset)
      pt2.add(offset)
    }
  })
  return true
}
/**
 * 生成用于展示的航线
 * @param {THREE.Vector3[]} wallPath 工作区本地空间的喷头坐标
 * @param {SprayConfig} config 航线配置
 * @param {WorkArea} sprayArea 工作区
 * @param {string} sprayColor 喷漆颜色，css颜色字符串
 * @param {THREE.Mesh|null} convexMesh 凸包网格
 * @return {SprayPath}
 */
function createSprayPathFromSprayPts(wallPath, config, sprayArea, sprayColor, convexMesh) {
  const dronePath = []
  if (!adjustWallPathBySurface(sprayArea, config, wallPath, dronePath, convexMesh)) {
    wallPath.forEach(pt => {
      const pos = pt.clone()
      pos.z = -config.sprayDistance
      pos.applyMatrix4(sprayArea.planeMat)
      dronePath.push(pos)
      pt.applyMatrix4(sprayArea.planeMat)
    })
  }
  const target = new SprayPath()
  target.distanceToWall = config.sprayDistance
  target.workAreaIds.push(sprayArea.id)
  target.facing = sprayArea.facing.clone()
  target.cssColorStr = sprayColor || defaultAreaColor
  target.color = new THREE.Color().setStyle(target.cssColorStr)
  target.wallPath = wallPath
  target.dronePath = dronePath
  wallPath.forEach((it, index) => {
    if (index === 0 || it.isStripeStart) {
      target._sprayFlags.push(false)
      target.needSprays.push(0)
    } else {
      target._sprayFlags.push(true)
      target.needSprays.push(1)
    }
    target.nozzleActions.push(NozzleActionEnum.keep)
  })
  target.editableRect.w = config.rectWidth
  target.editableRect.h = config.rectHeight
  target.boundingBox = new THREE.Box3().setFromPoints(dronePath)
  return target
}
/**
 * 只针对不带孔的矩形工作区
 * @param {SprayConfig} config
 * @param {WorkArea} sprayArea
 * @param {string|null} sprayColor
 * @return {SprayPath}
 */
export function generateRectAreaSprayPath(config, sprayArea, sprayColor) {
  const wallPath = computeRectSprayPath(config, sprayArea)
  return createSprayPathFromSprayPts(wallPath, config, sprayArea, sprayColor)
}

// 条状区域 (点串）
class Stripe {
  vBig = new THREE.Vector3()
  vSmall = new THREE.Vector3()
  _mid = new THREE.Vector3()
  /**
   * @type {{x: number} | {y: number}}
   */
  val
  fieldA
  fieldB
  keyVal = 0

  constructor(small, big, val, isVertical, scaleInv) {
    this.vSmall.x = small.x * scaleInv
    this.vSmall.y = small.y * scaleInv
    this.vBig.x = big.x * scaleInv
    this.vBig.y = big.y * scaleInv
    this._mid.copy(this.vSmall).lerp(this.vBig, 0.5)
    this.keyVal = val
    if (isVertical) {
      this.val = {x: val * scaleInv}
      this.fieldA = 'x'
      this.fieldB = 'y'
    } else {
      this.val = {y: val * scaleInv}
      this.fieldA = 'y'
      this.fieldB = 'x'
    }
  }
  otherPoint(point) {
    return point === this.vSmall ? this.vBig : this.vSmall
  }
  getSequence(point) {
    const d1 = point.distanceTo(this.vSmall)
    const d2 = point.distanceTo(this.vBig)
    if (d1 < d2) {
      return [this.vSmall, this.vBig]
    } else {
      return [this.vBig, this.vSmall]
    }
  }
  distanceToOther(other) {
    return this._mid.distanceTo(other._mid)
  }
}
/**
 * 寻找交点
 * @param {{x: number, y: number}} p0
 * @param {{x: number, y: number}} p1
 * @param {boolean} isVertical
 * @param {number} val
 * @return {{x: number, y: number}[]}
 */
function findCrossPts(p0, p1, isVertical, val) {
  const field = isVertical ? 'x' : 'y'
  let min, max, a, b
  if (p0[field] < p1[field]) {
    min = p0[field]
    max = p1[field]
    a = p0
    b = p1
  } else {
    min = p1[field]
    max = p0[field]
    if (p0[field] === p1[field]) {
      const field2 = isVertical ? 'y' : 'x'
      if (p0[field2] < p1[field2]) {
        a = p0
        b = p1
      } else {
        a = p1
        b = p0
      }
    } else {
      a = p1
      b = p0
    }
  }
  if (p0[field] === p1[field] && p0[field] === val) {
    return [a, b]
  } else if (p0[field] === val) {
    return [p0]
  } else if (p1[field] === val) {
    return [p1]
  }

  if (val < min || val > max) {
    return []
  }
  const alpha = (val - min) / (max - min)
  const x = Math.round(THREE.MathUtils.lerp(a.x, b.x, alpha))
  const y = Math.round(THREE.MathUtils.lerp(a.y, b.y, alpha))
  return [{x, y}]
}

/**
 * 射线撞击到孔洞后，调整一下坐标，尽量不要往不可喷涂区域喷
 * @param {{x: number, y: number}[]} segmentPoints 组成孔洞的边
 * @param {{x: number, y: number}} point 撞击在孔洞边上的坐标
 * @param {SprayConfig} config
 * @param {string} field
 * @param {boolean} isSmaller
 */
function adjustHitHolePoint(segmentPoints, point, config, field, isSmaller) {
  const ray0 = {}, ray1 = {}
  let cur = point[field]
  ray0[field] = cur - config.rectWidth * 0.5
  ray1[field] = cur + config.rectWidth * 0.5
  const isVertical = field === 'x'
  const hits = [cur]
  for (let i = 0; i < segmentPoints.length; i += 2) {
    const p0 = segmentPoints[i]
    const p1 = segmentPoints[i + 1]
    const arr0 = findCrossPts(p0, p1, isVertical, ray0[field])
    const arr1 = findCrossPts(p0, p1, isVertical, ray1[field])
    if (arr0.length === 2) { // 排除擦边情况
      arr0.length = 0
    }
    if (arr1.length === 2) { // 排除擦边情况
      arr1.length = 0
    }
    arr0.concat(arr1).forEach(pt => hits.push(pt[field]))
  }
  const hh = config.rectHeight * 0.5
  if (isSmaller) {
    cur = Math.max(...hits)
    point[field] = cur + hh
  } else {
    cur = Math.min(...hits)
    point[field] = cur - hh
  }
  return point
}
/**
 * 根据工作区和 单条射线生成若干个条状区域
 * @param {WorkArea} area
 * @param {number[]} segmentPtIndices
 * @param {{x: number} | {y: number}} ray
 * @param {SprayConfig} config
 * @return {Stripe[]}
 */
function createStripesByRay(area, segmentPtIndices, ray, config) {
  const isVerticalStripe = ray.x !== undefined
  const scale = 1000
  const scaleInv = 1.0 / scale
  const value = Math.round((isVerticalStripe ? ray.x : ray.y) * scale)
  const points = area.verts[0]
  const pts = segmentPtIndices.map(it => {
    const pt = points[it]
    const x = Math.round(pt.x * scale)
    const y = Math.round(pt.y * scale)
    return {x, y}
  })
  const crossPts = []
  const stripes = []
  const outestEdges = area.outestEdges
  const holePts = pts.slice(outestEdges * 2)
  for (let i = 0, edgeIndex = 0; i < pts.length; i += 2, edgeIndex++) {
    const p0 = pts[i]
    const p1 = pts[i + 1]
    const arr = findCrossPts(p0, p1, isVerticalStripe, value)
    arr.forEach(pt => {
      const existed = crossPts.find(point => point.x === pt.x && point.y === pt.y)
      if (existed) { // 交点去除重复
        return
      }
      pt.fromHole = edgeIndex >= outestEdges
      crossPts.push(pt)
    })
  }
  if (crossPts.length < 2) {
    return []
  }
  const field = isVerticalStripe ? 'x' : 'y'
  const other = isVerticalStripe ? 'y' : 'x'
  // 由小到大排列
  crossPts.sort((a, b) => a[other] - b[other])
  if (config.expand > 0) { // 处理航线的外扩参数
    const firstPt = crossPts[0]
    const lastPt = crossPts[crossPts.length - 1]
    firstPt[other] -= config.expand * scale
    lastPt[other] += config.expand * scale
  }
  for (let i = 0; i < crossPts.length - 1; i++) {
    const p0 = crossPts[i]
    const p1 = crossPts[i + 1]
    const mid = {x: (p0.x + p1.x) * 0.5 * scaleInv, y: (p0.y + p1.y) * 0.5 * scaleInv}
    const inside = area.containLocalPosition(mid)
    if (inside) {
      if (p0.fromHole) {
        adjustHitHolePoint(holePts, p0, config, field, true)
      } else if (p1.fromHole) {
        adjustHitHolePoint(holePts, p1, config, field, false)
      }
      if (p0[other] >= p1[other]) {
        continue
      }
      const stripe = new Stripe(p0, p1, value, isVerticalStripe, scaleInv)
      stripes.push(stripe)
    }
  }
  stripes.sort((a, b) => a.vSmall[other] - b.vSmall[other])
  return stripes
}
/**
 * 根据喷涂配置 和 工作区，生成若干个条状区域，这些条状区域是根据x坐标 和y坐标 组织起来的；
 * 可以很快捷的查询到一个条状区域的 同 x / y 的若干个区域，一个条状区域的相邻区域有哪些
 * @param {SprayConfig} config
 * @param {WorkArea} sprayArea
 */
function generateStripes(config, sprayArea) {
  const segmentPtIndices = sprayArea.initSegments()
  const isVertical = config.direction === DirectEnum.vertical
  const spacingWidth = config.spacingWidth <= 0 ? config.rectWidth * 0.5 : config.spacingWidth
  sprayArea.updateLocalBox()
  const localHomePoint = new THREE.Vector3().copy(config.flyHomePoint).sub(sprayArea.getCenterInWorld())
  localHomePoint.transformDirection(sprayArea.planeMatInv)
  const {min, max} = sprayArea.localBox3
  const field = isVertical ? 'x' : 'y'
  const rays = []
  let last = 0
  // 初始化若干个撞击射线
  for (let i = min[field]; i <= max[field]; i += spacingWidth) {
    last = i
    const ray = {}
    ray[field] = i
    rays.push(ray)
  }
  if (last < max[field]) {
    const ray = {}
    ray[field] = max[field]
    rays.push(ray)
  }
  // 生成每个撞击射线 对应的若干个条状区域
  const stripesMap = new Map()
  const stripeKeys = []
  for (let i = 0; i < rays.length; i++) {
    const ray = rays[i]
    const stripes = createStripesByRay(sprayArea, segmentPtIndices, ray, config)
    if (!stripes.length) {
      continue
    }
    const key = stripes[0].keyVal
    stripeKeys.push(key) // stripeKeys.push(ray[field])
    stripesMap.set(key, stripes)
  }
  return {stripeKeys, stripesMap, isVertical}
}
function findStripeInArr(curStripe, arr) {
  if (arr.length === 1) {
    return arr[0]
  }
  let min = Infinity, minIndex
  for (let i = 0; i < arr.length; i++) {
    const distance = arr[i].distanceToOther(curStripe)
    if (distance < min) {
      min = distance
      minIndex = i
    }
  }
  return arr[minIndex]
}
/**
 * 寻找下一个条状区域
 * @param {number[]} stripeKeys
 * @param {Map<number, Stripe[]>} stripesMap
 * @param {Stripe[]} stripes
 * @return {Stripe|null}
 */
function findNextStripe(stripeKeys, stripesMap, stripes) {
  const curStripe = stripes[stripes.length - 1]
  const prev = stripes[stripes.length - 2]
  const isForward = prev ? prev.keyVal < curStripe.keyVal : true
  let index = stripeKeys.indexOf(curStripe.keyVal)
  let indexNext
  // 优先从另外一列寻找条状区域
  if (index < 0) {
    // 上一次调用 findNextStripe 返回null后，会跑到这里来
    indexNext = isForward ? stripeKeys.length - 1 : 0
  } else {
    if (isForward) {
      indexNext = index < stripeKeys.length - 1 ? index + 1 : index - 1
    } else {
      indexNext = !index ? 1 : index - 1
    }
  }
  let arrNext = stripesMap.get(stripeKeys[indexNext])
  // arrNext = arrNext.filter(it => !stripes.includes(it))
  if (!arrNext || !arrNext.length) {
    // 换列寻找条状区域失败，从当前列寻找
    arrNext = stripesMap.get(curStripe.keyVal).filter(it => !stripes.includes(it))
    if (!arrNext.length) {
      return null
    }
  }
  return findStripeInArr(curStripe, arrNext)
}
/**
 * @param {number[]} stripeKeys
 * @param {Map<number, Stripe[]>} stripesMap
 */
function clearStripesMap(stripeKeys, stripesMap) {
  const stripesMapNew = new Map()
  const keys = []
  stripesMap.forEach((value, key) => {
    if (value.length) {
      keys.push(key)
      stripesMapNew.set(key, value)
    }
  })
  keys.sort((a, b) => a - b)
  // return {stripeKeys: keys, stripesMap: stripesMapNew}
  return [keys, stripesMapNew]
}
/**
 * 支持生成带孔的不规则平面区域的航线
 * @param {SprayConfig} config
 * @param {WorkArea} sprayArea
 * @param {string|null} sprayColor
 * @param {THREE.Mesh|null} convexMesh
 * @return {SprayPath}
 */
export function generateSprayPath(config, sprayArea, sprayColor, convexMesh) {
  let {stripeKeys, stripesMap} = generateStripes(config, sprayArea)
  const last = stripeKeys[stripeKeys.length - 1]
  const arrFirst = stripesMap.get(stripeKeys[0])
  const arrLast = stripesMap.get(last)
  const v0 = arrLast[arrLast.length - 1].vBig
  const v3 = arrLast[0].vSmall
  const v1 = arrFirst[arrFirst.length - 1].vBig
  const v2 = arrFirst[0].vSmall
  const arr = [v0, v1, v2, v3]
  const stripes0 = [arrLast[arrLast.length - 1], arrFirst[arrFirst.length - 1], arrFirst[0], arrLast[0]]
  const localHomePoint = new THREE.Vector3().copy(config.flyHomePoint).applyMatrix4(sprayArea.planeMatInv)
  const distances = arr.map(it => it.distanceTo(localHomePoint))
  const minDist = Math.min(...distances)
  const minIndex = distances.indexOf(minDist)
  const startPoint = arr[minIndex]
  let curStripe = stripes0[minIndex]
  let cur = curStripe.otherPoint(startPoint)
  const stripes = [curStripe]
  const pts = [startPoint, cur]
  removeArrayItem(stripesMap.get(curStripe.keyVal), curStripe)
  while(true) {
    const stripeNext = findNextStripe(stripeKeys, stripesMap, stripes)
    if (!stripeNext) {
      const [keys, map] = clearStripesMap(stripeKeys, stripesMap)
      if (!keys.length) {
        break
      }
      stripeKeys = keys
      stripesMap = map
      continue
    }
    stripes.push(stripeNext)
    curStripe = stripeNext
    const seq = curStripe.getSequence(cur)
    seq[0].isStripeStart = true
    pts.push(...seq)
    cur = seq[seq.length - 1]
    removeArrayItem(stripesMap.get(stripeNext.keyVal), stripeNext)
  }
  return createSprayPathFromSprayPts(pts, config, sprayArea, sprayColor, convexMesh)
}
